
package jwww.ml;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import jwww.util.QuickProcess;

import org.w3c.dom.*;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/** Simple support class to help open/build/parse/write XML.
 * <br>
 * @author striegel, jwozniak
 * */
public class XML
{
  static DocumentBuilder builder = null;
  static TransformerFactory     tFactory = null;

  static boolean ready = false;

  public static void init()
  {
    if (ready)
      return;
    else
      ready = true;

    boolean replaceXML = false;

    if (replaceXML)
    {
      // Ensure up-to-date XML tools.
      // Default version has memory problems.
      System.setProperty
        ("javax.xml.transform.TransformerFactory",
         "org.apache.xalan.processor.TransformerFactoryImpl");
      System.setProperty
        ("javax.xml.parsers.DocumentBuilderFactory",
         "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
      System.setProperty
        ("javax.xml.parsers.SAXParserFactory",
         "org.apache.xerces.jaxp.SAXParserFactoryImpl");
    }

    try
    {
      DocumentBuilderFactory dFactory =
        DocumentBuilderFactory.newInstance();
      builder = dFactory.newDocumentBuilder();

      tFactory =
        TransformerFactory.newInstance();

    }
    catch (ParserConfigurationException e)
    {
      e.printStackTrace();
    }
  }

  /** Starting at the root argument,
      search through any children
      and populate the returned Vector with any child Elements.
      @param root The base (or root) node to use as the
                  parent for the search. The search does
                  not traverse below the first level
      @return Non-null: A valid reference to a Vector object.
                 May have 0 elements.

  */
  public static List<Element> find(Element root)
  {
    if (root == null)
    {
      System.err.println("XML.find(): received null!");
      return null;
    }

    List<Element> matches = new ArrayList<Element>();

    NodeList list = root.getChildNodes();

    for (int j = 0; j < list.getLength(); j++)
    {
      Node node = list.item(j);

      if (node.getNodeType() == Node.ELEMENT_NODE)
        matches.add((Element) node);
    }
    return matches;
  }

  /** Starting at the root argument,
      search through any children
      and populate the returned Vector with any children
      whose node name matches the requested string.
      @param root The base (or root) node to use as the
                    parent for the search. The search does
                    not traverse below the first level
      @param name The name of the search string to match.
                  Wildcards are not currently supported.
      @return A Vector of matching Elements, or null on error.
             The Vector object may contain zero
             elements if no matches were found in the listing.
  */
  public static List<Element> find(Element root, String name)
  {
    if (root == null)
    {
      System.err.println("XML.find(): received null!");
      return null;
    }

    List<Element> matches = new ArrayList<Element>();
    NodeList list = root.getChildNodes();
    for (int j = 0; j < list.getLength(); j++)
    {
      Node node = list.item(j);
      if (node.getNodeType() == Node.ELEMENT_NODE &&
          node.getNodeName().compareToIgnoreCase(name) == 0)
        matches.add((Element) node);
    }
    return matches;
  }


  public static void remove(Element root, String name)
  {
    List<Element> matches = find(root, name);
    for (int i = 0; i < matches.size(); i++)
    {
      root.removeChild((Element) matches.get(i));
    }
  }

  /** Search for Element at Document level
      @param root Start at this element
      @param name Case-insensitive tag name to find.
      @return The first matching subelement or null if not found.
  */
  public static Element top(Document root, String name)
  {
    if (root == null)
      return null;

    NodeList list = root.getChildNodes();
    for (int j = 0; j < list.getLength(); j++)
    {
      Node node = list.item(j);
      if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE &&
          node.getNodeName().compareToIgnoreCase(name) == 0)
        return (Element) node;
    }
    // If we didn't find it, we're done anyway:
    return null;
  }

  /** Search for Element at Document level
      @param root Start at this element
      @return The first matching subelement or null if not found.
  */
  public static Element top(Document root)
  {
    if (root == null)
      return null;

    NodeList list = root.getChildNodes();
    for (int j = 0; j < list.getLength(); j++)
    {
      Node node = list.item(j);
      if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE)
        return (Element) node;
    }
    // If we didn't find it, we're done anyway:
    return null;
  }

  /** Search for Element under a given Element.
      @param root Start at this element
      @param name Case-insensitive tag name to find.
      @return The first matching subelement or null if not found.
  */
  public static Element top(Element root, String name)
  {
    if (root == null)
      return null;

    NodeList list = root.getChildNodes();
    for (int j = 0; j < list.getLength(); j++)
    {
      Node node = list.item(j);
      if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE &&
          node.getNodeName().compareToIgnoreCase(name) == 0)
        return (Element) node;
    }
    // If we didn't find it, we're done anyway:
    return null;
  }

  /** Search for first child Element
      @param root Start at this element
      @return The first matching subelement or null if not found.
  */
  public static Element top(Element root)
  {
    if (root == null)
      return null;

    NodeList list = root.getChildNodes();
    for (int j = 0; j < list.getLength(); j++)
    {
      Node node = list.item(j);
      if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE)
        return (Element) node;
    }
    // If we didn't find it, we're done anyway:
    return null;
  }


  /** Return the text value of this Element.
      Only returns one sub-text-node worth of data.
      If nothing, return empty String;
      @param target The Element to consider.
      @return The value of the Element if text,
                empty String if nothing there,
                null on error.
   */
  public static String text(Element target)
  {
    String result = "";

    if (target == null)
      return null;

    NodeList list = target.getChildNodes();
    if (list.getLength() >= 1)
    {
      Node node = list.item(0);
      if (node.getNodeType() == Node.TEXT_NODE)
        result = node.getNodeValue();
    }
    return result.trim();
  }

  /** Converts the value of the Element to an integer.
      @param target The Element to query for a value.
      @return The value of the target as an int.
  */
  public static int integer(Element target)
  {
    int tmp = 0;
    if (target == null)
      return(tmp);

    String text = text(target);
    if (text != null && text.length() > 0)
    {
      try
      {
        tmp = Integer.parseInt(text);
      }
      catch(NumberFormatException nfe)
      {
        System.out.println("Bad number for node " +
                           target.getNodeName());
        System.out.println("Value: " +
                           target.getNodeValue());
        System.out.println("Value2: " + text);
      }
      catch(DOMException de)
      {
        System.out.println
          ("Bad value for node " + target.getNodeName() +
           " in getIntValueFromChild");
      }
    }
    return tmp;
  }

  /** This function simplifies searching for the data value
      located inside an
      element by locating the element itself and then
      automatically traversing one
      level down to grab the actual data from the element.
      @param root The base node from which to conduct the search
      @param name The name to look for underneath the search node
      (the element name that will yield a match)
      @return Null: The element was not found.
      Non-Null: The element was found the resultant string
      from the getNodeValue
      function is being returned.
  */
  public static String text(Element root, String name)
  {
    String result = null;
    Element match = top(root, name);
    if (match != null)
      result = text(match);
    return result;
  }

  /** Find the Element, then grab its integer value.
      Returns 0 if no element found.
  */
  public static int integer(Element root, String name)
  {
    int result = 0;
    Element match = top(root, name);
    result = integer(match);
    return result;
  }

  public static String[] typeName =
  {
    "none",
    "Element",
    "Attr",
    "Text",
    "CDATA",
    "EntityRef",
    "Entity",
    "ProcInstr",
    "Comment",
    "Document",
    "DocType",
    "DocFragment",
    "Notation",
  };

  // Return a string that identifies this node
  //     in the tree

  /** Parse an XML node and display the contents of that specific node.
      Note that
      this function does not display the number of children nor
      any contents of
      the children themselves either.
      This function was taken from the Java XML
      tutorial on Sun's website.
      @param domNode The node for which the data will be displayed
      @return A valid reference to a string that
      contains the parsed information.
  */
  public static String toString(org.w3c.dom.Node domNode)
  {
    String s = typeName[domNode.getNodeType()];
    String nodeName = domNode.getNodeName();
    if (! nodeName.startsWith("#")) {
      s += ": " + nodeName;
    }
    if (domNode.getNodeValue() != null) {
      if (s.startsWith("ProcInstr"))
        s += ", ";
      else
        s += ": ";
      // Trim the value to get rid of NL's at the front
      String t = domNode.getNodeValue().trim();
      int x = t.indexOf("\n");
      if (x >= 0) t = t.substring(0, x);
      s += t;
    }
    return s;
  }

  /** Open an XML file and return it as a DOM.
      @param filename The file to open.
      @return The DOM or null on an error.
  */
  public static Document open(String filename)
  {
    DocumentBuilderFactory dbFactory;
    DocumentBuilder dBuilder;
    Document document;

    dbFactory = DocumentBuilderFactory.newInstance();
    dbFactory.setNamespaceAware(true);
    try{
      dBuilder = dbFactory.newDocumentBuilder();
      File iFile = new File(filename);
      if (!iFile.exists())
        return(null);
      document = dBuilder.parse(iFile);
      return(document);
    }catch (SAXException sxe) {
      //      Exception  x = sxe;
      //      if (sxe.getException() != null){
      //        x = sxe.getException();
      //      }
      //      x.printStackTrace();
    }catch (ParserConfigurationException pce) {
      ;//pce.printStackTrace();
    }catch (IOException ioe) {
      ;//ioe.printStackTrace();
    }
    return(null);
  }

  /** Parse a String and return it as a DOM.
      @param s The String to parse.
      @return The DOM or null on an error.
  */
  public static Document parse(String s)
  {
    Document doc = XML.blank();

    try
    {
      Transformer transformer = tFactory.newTransformer();
      StringReader sr     = new StringReader(s);
      StreamSource source = new StreamSource(sr);
      DOMResult result = new DOMResult(doc);
      transformer.transform(source, result);
      sr.close();
    }
    catch (TransformerConfigurationException tce)
    {
      // Error generated by the parser
      System.out.println ("\n** Transformer Factory error");
      System.out.println("   " + tce.getMessage() );

      // Use the contained exception, if any
      Throwable x = tce;
      if (tce.getException() != null)
        x = tce.getException();
      x.printStackTrace();
      return null;

    }
    catch (TransformerException te)
    {
      // Error generated by the parser
      System.out.println ("\n** Transformation error");
      System.out.println("   " + te.getMessage() );

      // Use the contained exception, if any
      Throwable x = te;
      if (te.getException() != null)
        x = te.getException();
      x.printStackTrace();
      if (x instanceof SAXParseException)
        System.err.println
          ("l:" + ((SAXParseException) x).getLineNumber() +
           "c:" + ((SAXParseException) x).getColumnNumber());
      return null;
    }

    return doc;
  }

  /** Parse a stream.
      @param in The stream to parse
      @return The resulting DOM.
  */
  public static Document parse(InputStream in)
  {
    Document outDoc = null;

    try
    {
      outDoc = builder.parse(in);
    }
    catch(Exception e)
    {
      System.err.println(e);
    }

    return outDoc;
  }

  public static Document exec(String command)
  {
    QuickProcess qp     = new QuickProcess(command);
    Document     result = parse(qp.output());
    return result;
  }

  /** Create a new blank document for populating with values.
      @return The blank DOM.
  */
  public static Document blank()
  {
    Document result = null;

    try
    {
      result = builder.newDocument();
    }
    catch (Exception e)
    {
      System.out.println("XML.blank(): " + e);
    }

    return result;
  }

  /** Write a DOM to an actual file.
      @param doc The Document to write.
      @param filename The target path for the new file.
  */
  public static void write(Document doc, String filename)
    throws IOException
  {
    if (doc == null)
    {
      doc = blank();
      add(doc, "NULL");
    }
    if (filename == null)
    {
      System.out.println("XML.write(): Null filename.");
      return;
    }
    if (filename.length() == 0)
    {
      System.out.println("XML.write(): Empty filename.");
      return;
    }

    try
    {
      // Use a Transformer for output
      Transformer transformer = tFactory.newTransformer();
      transformer.setOutputProperty("indent", "yes");
      DOMSource source = new DOMSource(doc);
      File file = new File(filename);
      file.createNewFile();
      BufferedOutputStream output =
        new BufferedOutputStream(new FileOutputStream(file));
      StreamResult result = new StreamResult(output);
      transformer.transform(source, result);
      output.close();
    }
    catch (TransformerConfigurationException tce)
    {
      // Error generated by the parser
      System.out.println ("\n** Transformer Factory error");
      System.out.println("   " + tce.getMessage() );

      // Use the contained exception, if any
      Throwable x = tce;
      if (tce.getException() != null)
        x = tce.getException();
      x.printStackTrace();

    }
    catch (TransformerException te)
    {
      // Error generated by the parser
      System.out.println ("\n** Transformation error");
      System.out.println("   " + te.getMessage() );

      // Use the contained exception, if any
      Throwable x = te;
      if (te.getException() != null)
        x = te.getException();
      x.printStackTrace();
    }
  }

  /** Prints the XML to stdout
      Similar to write().
  */
  public static void write(Document root)
  {
    if (root == null)
    {
      System.out.println("<NULL>");
      return;
    }

    try {
      // Use a Transformer for output

      Transformer transformer = tFactory.newTransformer();
      transformer.setOutputProperty("indent", "yes");
      DOMSource source = new DOMSource(root);
      StreamResult result = new StreamResult(System.out);
      transformer.transform(source, result);
    } catch (TransformerConfigurationException tce) {
      // Error generated by the parser
      System.out.println ("\n** Transformer Factory error");
      System.out.println("   " + tce.getMessage() );

      // Use the contained exception, if any
      Throwable x = tce;
      if (tce.getException() != null)
        x = tce.getException();
      x.printStackTrace();

    } catch (TransformerException te) {
      // Error generated by the parser
      System.out.println ("\n** Transformation error");
      System.out.println("   " + te.getMessage() );

      // Use the contained exception, if any
      Throwable x = te;
      if (te.getException() != null)
        x = te.getException();
      x.printStackTrace();
    }
  }

  /** Prints the XML to a String.
      Similar to write().
  */
  public static String string(Document doc)
  {
    return buffer(doc).toString();
  }

  /** Prints the XML to a StringBuffer.
      Similar to writeFile().
  */
  public static StringBuffer buffer(Document doc)
  {
    StringBuffer sb = null;

    if (doc == null)
      return null;

    StringWriter sw = new StringWriter(1000);
    try
    {
      // Use a Transformer for output
      Transformer transformer = tFactory.newTransformer();
      transformer.setOutputProperty("indent", "yes");
      DOMSource source = new DOMSource(doc);
      StreamResult result = new StreamResult(sw);
      transformer.transform(source, result);
      sb = sw.getBuffer();
      sw.close();
    }
    catch (TransformerConfigurationException tce)
    {
      // Error generated by the parser
      System.out.println ("\n** Transformer Factory error");
      System.out.println("   " + tce.getMessage() );

      // Use the contained exception, if any
      Throwable x = tce;
      if (tce.getException() != null)
        x = tce.getException();
      x.printStackTrace();

    }
    catch (TransformerException te)
    {
      // Error generated by the parser
      System.out.println ("\n** Transformation error");
      System.out.println("   " + te.getMessage() );

      // Use the contained exception, if any
      Throwable x = te;
      if (te.getException() != null)
        x = te.getException();
      x.printStackTrace();
    }
    catch (IOException e)
    {
      e.printStackTrace();
    }
    return sb;
  }

  /** This function simplifies setting text for the data value
      located inside an
      element by locating the element itself and then
      automatically traversing one
      level down to set the actual text data in the element.
      @param root The base node from which to conduct the search
      @param name The name to look for underneath the search node
      (the element name that will yield a match)
      @param value The value to use.
      @return false: The text value was not set.
      true: The text node was updated
  */
  public static boolean set(Element root, String name, String value)
  {
    boolean success = false;

    Element elem = top(root, name);

    if (elem == null)
      return success;

    NodeList list = elem.getChildNodes();

    // There is no current text node.  Create one and assign it's value
    if (list.getLength() < 1)
    {
      Node data = elem.getOwnerDocument().createTextNode(value);
      elem.appendChild(data);
      success = true;
      return success;
    }

    // There is a text node.  Verify its type and set the new value
    if (list.getLength() == 1)
    {
      Node node = list.item(0);
      if (node.getNodeName() == "#text")
      {
        node.setNodeValue(value);
        success = true;
      }
      else
      {
        System.err.println("Error: Node was not type - Text");
        success = false;
      }
    }

    // There are multiple subnodes for this parent --- error
    if (list.getLength() > 1) {
      System.err.println("Error: Extra values for the element data");
    }

    return success;
  }

    /** This function simplifies setting text for the data value
      located inside an
      element by locating the element itself and then
      automatically traversing one
      level down to set the actual text data in the element.
      @param root The base node from which to conduct the search
      @param name The name to look for underneath the search node
      (the element name that will yield a match)
      @param value The value to use.
      @return false: The text value was not set.
      true: The text node was updated
  */
  public static boolean set(Element root, String name, int value)
  {
    return set(root, name, "" + value);
  }


  /** Give an Element node another text value.
      @param root The node to work with.
      @param value The value string.
  */
  public static void set(Element root, String value)
  {
    if (root == null)
      System.err.println("XML.setElementValue(): got null Node");
    if (value == null)
      System.err.println("XML.setElementValue(): got null String");

    Document doc = root.getOwnerDocument();
    Node text = doc.createTextNode(value);
    root.appendChild(text);
  }

  /** Give an Element node another text value.
      @param root The node to work with.
      @param value The value.
  */
  public static void set(Element root, int value)
  {
    set(root, "" + value);
  }

  /** Add an element/value combination.
    This function will an element and then embed the value
    as a text node beneath the element.
    @param root The parent node under
                which this element/value will be attached.
    @param name Name of the element that will be
                inserted under the parent.
    @param value The value (text value) to be listed
                 underneath the element name.
    @return The new Element
            or null if given anything null or a 0-length name.
  */
  public static Element add(Element root, String name, String value)
  {
    Document doc = root.getOwnerDocument();

    if (root  == null ||
        name  == null ||
        value == null ||
        name.length() == 0)
      return null;

    Element elem = doc.createElement(name);
    root.appendChild(elem);

    Node node = doc.createTextNode(value);
    elem.appendChild(node);
    return elem;
  }

  /** Add an element/value combination.
    This function will an element
    and then embed the value information as a text node beneath
    the element.
    @param root The parent node under
                which this element/value will be attached
    @param name name of the element that will be
           inserted under the parent.
    @param value The value (text value) to be listed
                   underneath the element name.
  */
  public static Element add(Element root, String name, int value)
  {
    return add(root, name, "" + value);
  }

  /** Add a new element underneath the parent Element
    specified in the argument list.
    This function will then return the newly created Element.
    @param root The Element under which to insert the new Element.
    @param name The name for the new Element.
    @return The newly created element. A null value indicates that
              one of the inputs to the function had an error.
   */

  public static Element add(Element root, String name)
  {
    if (root == null    ||
        name == null    ||
        name.length() == 0)
      return null;

    Document doc = root.getOwnerDocument();

    Node elem = doc.createElement(name);
    root.appendChild(elem);

    return (Element) elem;
  }

  public static Element add(Document doc, String name)
  {
    if (doc == null  ||
        name == null ||
        name.length() == 0)
      return null;

    Element elem = doc.createElement(name);
    doc.appendChild(elem);
    return elem;
  }


  /** Copy a whole Document
      @param doc The Document to copy.
      @return A copy.
  */
  public static Document clone(Document doc)
  {
    Document output = null;
    try
    {
      output = builder.newDocument();
      Transformer transformer = tFactory.newTransformer();
      DOMSource source = new DOMSource(doc);
      DOMResult result = new DOMResult(output);
      transformer.transform(source, result);
    }
    catch (Exception e)
    {
      System.err.println(e);
    }
    return output;
  }

  /** Copy an Element and its children from one Document
      recursively to under a node in another Document.
      @param source The Element to copy.
      @param target The Element under which the new Element will
                            be placed.
      @return The new Element, as a child of target.  Null on error.
  */
  public static Element copy(Element source, Element target)
  {
    if (source == null)
    {
      System.out.println("XML.copy(): source null.");
      System.exit(2);
    }
    if (target == null)
    {
      System.out.println("XML.copy(): target null.");
      System.exit(2);
    }

    Document targetDoc = target.getOwnerDocument();

    Node newNode = null;
    try
    {
      newNode = targetDoc.importNode(source, true);
      target.appendChild(newNode);
    }
    catch (DOMException e)
    {
      System.err.println(e);
    }
    return (Element) newNode;
  }

  /** Debugging use only.
   */
  public static void main(String[] args)
  {
    try
    {
      Document d = blank();

      Element nd = add(d, "top");
      add(nd, "child", "v");
      write(d);
      Document c = clone(d);
      write(c);
      Element a = add(nd, "extra");
      add(a, "another");
      write(d);
      write(c);
    }
    catch (Exception e)
    {
      System.err.println(e);
    }
  }

}
